/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.tags.form;

import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.servlet.support.BindStatus;

import javax.servlet.jsp.JspException;
import java.util.Collection;
import java.util.Map;

/**
 * The {@code <select>} tag renders an HTML 'select' element.
 * Supports data binding to the selected option.
 *
 * <p>Inner '{@code option}' tags can be rendered using one of the
 * approaches supported by the OptionWriter class.
 *
 * <p>Also supports the use of nested {@link OptionTag OptionTags} or
 * (typically one) nested {@link OptionsTag}.
 *
 * <p>
 * <table>
 * <caption>Attribute Summary</caption>
 * <thead>
 * <tr>
 * <th class="colFirst">Attribute</th>
 * <th class="colOne">Required?</th>
 * <th class="colOne">Runtime Expression?</th>
 * <th class="colLast">Description</th>
 * </tr>
 * </thead>
 * <tbody>
 * <tr class="altColor">
 * <td><p>accesskey</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>cssClass</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>cssErrorClass</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute. Used when the bound field has errors.</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>cssStyle</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>dir</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>disabled</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute. Setting the value of this attribute to 'true'
 * will disable the HTML element.</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>htmlEscape</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>Enable/disable HTML escaping of rendered values.</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>id</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>itemLabel</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>Name of the property mapped to the inner text of the 'option' tag</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>items</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>The Collection, Map or array of objects used to generate the inner
 * 'option' tags</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>itemValue</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>Name of the property mapped to 'value' attribute of the 'option'
 * tag</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>lang</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>multiple</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onblur</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>onchange</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onclick</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>ondblclick</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onfocus</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>onkeydown</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onkeypress</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>onkeyup</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onmousedown</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>onmousemove</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onmouseout</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>onmouseover</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>onmouseup</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Event Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>path</p></td>
 * <td><p>true</p></td>
 * <td><p>true</p></td>
 * <td><p>Path to property for data binding</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>size</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Optional Attribute</p></td>
 * </tr>
 * <tr class="altColor">
 * <td><p>tabindex</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * <tr class="rowColor">
 * <td><p>title</p></td>
 * <td><p>false</p></td>
 * <td><p>true</p></td>
 * <td><p>HTML Standard Attribute</p></td>
 * </tr>
 * </tbody>
 * </table>
 *
 * @author Rob Harrop
 * @author Juergen Hoeller
 * @see OptionTag
 * @since 2.0
 */
@SuppressWarnings("serial")
public class SelectTag extends AbstractHtmlInputElementTag {

    /**
     * The {@link javax.servlet.jsp.PageContext} attribute under
     * which the bound value is exposed to inner {@link OptionTag OptionTags}.
     */
    public static final String LIST_VALUE_PAGE_ATTRIBUTE =
            "org.springframework.web.servlet.tags.form.SelectTag.listValue";

    /**
     * Marker object for items that have been specified but resolve to null.
     * Allows to differentiate between 'set but null' and 'not set at all'.
     */
    private static final Object EMPTY = new Object();


    /**
     * The {@link Collection}, {@link Map} or array of objects used to generate
     * the inner '{@code option}' tags.
     */
    @Nullable
    private Object items;

    /**
     * The name of the property mapped to the '{@code value}' attribute
     * of the '{@code option}' tag.
     */
    @Nullable
    private String itemValue;

    /**
     * The name of the property mapped to the inner text of the
     * '{@code option}' tag.
     */
    @Nullable
    private String itemLabel;

    /**
     * The value of the HTML '{@code size}' attribute rendered
     * on the final '{@code select}' element.
     */
    @Nullable
    private String size;

    /**
     * Indicates whether or not the '{@code select}' tag allows
     * multiple-selections.
     */
    @Nullable
    private Object multiple;

    /**
     * The {@link TagWriter} instance that the output is being written.
     * <p>Only used in conjunction with nested {@link OptionTag OptionTags}.
     */
    @Nullable
    private TagWriter tagWriter;

    /**
     * Returns '{@code true}' for arrays, {@link Collection Collections}
     * and {@link Map Maps}.
     */
    private static boolean typeRequiresMultiple(Class<?> type) {
        return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type));
    }

    /**
     * Get the value of the '{@code items}' attribute.
     * <p>May be a runtime expression.
     */
    @Nullable
    protected Object getItems() {
        return this.items;
    }

    /**
     * Set the {@link Collection}, {@link Map} or array of objects used to
     * generate the inner '{@code option}' tags.
     * <p>Required when wishing to render '{@code option}' tags from
     * an array, {@link Collection} or {@link Map}.
     * <p>Typically a runtime expression.
     *
     * @param items the items that comprise the options of this selection
     */
    public void setItems(@Nullable Object items) {
        this.items = (items != null ? items : EMPTY);
    }

    /**
     * Get the value of the '{@code itemValue}' attribute.
     * <p>May be a runtime expression.
     */
    @Nullable
    protected String getItemValue() {
        return this.itemValue;
    }

    /**
     * Set the name of the property mapped to the '{@code value}'
     * attribute of the '{@code option}' tag.
     * <p>Required when wishing to render '{@code option}' tags from
     * an array or {@link Collection}.
     * <p>May be a runtime expression.
     */
    public void setItemValue(String itemValue) {
        this.itemValue = itemValue;
    }

    /**
     * Get the value of the '{@code itemLabel}' attribute.
     * <p>May be a runtime expression.
     */
    @Nullable
    protected String getItemLabel() {
        return this.itemLabel;
    }

    /**
     * Set the name of the property mapped to the label (inner text) of the
     * '{@code option}' tag.
     * <p>May be a runtime expression.
     */
    public void setItemLabel(String itemLabel) {
        this.itemLabel = itemLabel;
    }

    /**
     * Get the value of the '{@code size}' attribute.
     */
    @Nullable
    protected String getSize() {
        return this.size;
    }

    /**
     * Set the value of the HTML '{@code size}' attribute rendered
     * on the final '{@code select}' element.
     */
    public void setSize(String size) {
        this.size = size;
    }

    /**
     * Get the value of the HTML '{@code multiple}' attribute rendered
     * on the final '{@code select}' element.
     */
    @Nullable
    protected Object getMultiple() {
        return this.multiple;
    }


    /**
     * Renders the HTML '{@code select}' tag to the supplied
     * {@link TagWriter}.
     * <p>Renders nested '{@code option}' tags if the
     * {@link #setItems items} property is set, otherwise exposes the
     * bound value for the nested {@link OptionTag OptionTags}.
     */
    @Override
    protected int writeTagContent(TagWriter tagWriter) throws JspException {
        tagWriter.startTag("select");
        writeDefaultAttributes(tagWriter);
        if (isMultiple()) {
            tagWriter.writeAttribute("multiple", "multiple");
        }
        tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize())));

        Object items = getItems();
        if (items != null) {
            // Items specified, but might still be empty...
            if (items != EMPTY) {
                Object itemsObject = evaluate("items", items);
                if (itemsObject != null) {
                    final String selectName = getName();
                    String valueProperty = (getItemValue() != null ?
                            ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
                    String labelProperty = (getItemLabel() != null ?
                            ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
                    OptionWriter optionWriter =
                            new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) {
                                @Override
                                protected String processOptionValue(String resolvedValue) {
                                    return processFieldValue(selectName, resolvedValue, "option");
                                }
                            };
                    optionWriter.writeOptions(tagWriter);
                }
            }
            tagWriter.endTag(true);
            writeHiddenTagIfNecessary(tagWriter);
            return SKIP_BODY;
        } else {
            // Using nested <form:option/> tags, so just expose the value in the PageContext...
            tagWriter.forceBlock();
            this.tagWriter = tagWriter;
            this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus());
            return EVAL_BODY_INCLUDE;
        }
    }

    /**
     * If using a multi-select, a hidden element is needed to make sure all
     * items are correctly unselected on the server-side in response to a
     * {@code null} post.
     */
    private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException {
        if (isMultiple()) {
            tagWriter.startTag("input");
            tagWriter.writeAttribute("type", "hidden");
            String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName();
            tagWriter.writeAttribute("name", name);
            tagWriter.writeAttribute("value", processFieldValue(name, "1", "hidden"));
            tagWriter.endTag();
        }
    }

    private boolean isMultiple() throws JspException {
        Object multiple = getMultiple();
        if (multiple != null) {
            String stringValue = multiple.toString();
            return ("multiple".equalsIgnoreCase(stringValue) || Boolean.parseBoolean(stringValue));
        }
        return forceMultiple();
    }

    /**
     * Set the value of the HTML '{@code multiple}' attribute rendered
     * on the final '{@code select}' element.
     */
    public void setMultiple(Object multiple) {
        this.multiple = multiple;
    }

    /**
     * Returns '{@code true}' if the bound value requires the
     * resultant '{@code select}' tag to be multi-select.
     */
    private boolean forceMultiple() throws JspException {
        BindStatus bindStatus = getBindStatus();
        Class<?> valueType = bindStatus.getValueType();
        if (valueType != null && typeRequiresMultiple(valueType)) {
            return true;
        } else if (bindStatus.getEditor() != null) {
            Object editorValue = bindStatus.getEditor().getValue();
            if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Closes any block tag that might have been opened when using
     * nested {@link OptionTag options}.
     */
    @Override
    public int doEndTag() throws JspException {
        if (this.tagWriter != null) {
            this.tagWriter.endTag();
            writeHiddenTagIfNecessary(this.tagWriter);
        }
        return EVAL_PAGE;
    }

    /**
     * Clears the {@link TagWriter} that might have been left over when using
     * nested {@link OptionTag options}.
     */
    @Override
    public void doFinally() {
        super.doFinally();
        this.tagWriter = null;
        this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE);
    }

}
